"""Shopify synchronization endpoints."""

from __future__ import annotations

import json
from typing import Dict, Optional, Tuple

import frappe
from frappe import _, _dict
from dateutil import parser as date_parser

from frappe.utils import get_datetime, now_datetime, cint, getdate, flt
from frappe.defaults import get_global_default

from ..utils.shopify_client import ShopifyClient, ShopifyClientError
from .api import create_yunexpress_order, WDT_WAREHOUSE_CODE, build_wdt_delivery_data, cancel_yunexpress_order, wdt

DEFAULT_ORDER_SINCE = "2000-01-01T00:00:00Z"
DEFAULT_ORDER_BATCH_SIZE = 50
DEFAULT_ORDER_MAX_PAGES = 1

DEFAULT_SKU_SINCE = "2000-01-01T00:00:00Z"
DEFAULT_SKU_BATCH_SIZE = 50
DEFAULT_SKU_MAX_PAGES = 1


class MissingSKUError(Exception):
    """Raised when an order references Item codes that are not available in ERP."""


def _make_sync_batch_id() -> str:
    return frappe.generate_hash(length=12)


def _get_allowed_warehouses(shop_name: str) -> list:
    """获取店铺允许发货的仓库列表 - 必须配置"""
    try:
        shop_doc = frappe.get_doc("usershop", shop_name)
        allowed_warehouses = getattr(shop_doc, 'allowed_warehouses', '[]')
                
        if isinstance(allowed_warehouses, str):
            if not allowed_warehouses.strip():
                frappe.logger().error(f"[Shopify] 店铺 {shop_name} 未配置允许的仓库列表 (空字符串)")
                return []
            parsed_warehouses = json.loads(allowed_warehouses)
            return parsed_warehouses
        
        if not allowed_warehouses:
            frappe.logger().error(f"[Shopify] 店铺 {shop_name} 未配置允许的仓库列表 (空值)")
            return []
            
        return allowed_warehouses
    except json.JSONDecodeError as e:
        frappe.logger().error(f"[Shopify] 店铺 {shop_name} 仓库配置格式错误: {str(e)}, 原始值: {allowed_warehouses}")
        return []
    except Exception as e:
        frappe.logger().error(f"[Shopify] 获取允许仓库列表失败: {str(e)}")
        return []


def _get_fulfillment_location(order: Dict) -> str:
    """从订单中提取履约仓库名称"""
    fulfillment_orders = order.get("fulfillmentOrders", {}).get("edges", [])
    
    if fulfillment_orders:
        assigned = fulfillment_orders[0].get("node", {}).get("assignedLocation", {})
        
        if assigned:
            # 直接从 assignedLocation.name 获取仓库名称
            location_name = assigned.get("name", "")
            return location_name
        else:
            frappe.logger().error(f"[Shopify] 订单 {order.get('name', order.get('id'))} assignedLocation 为空")
    else:
        frappe.logger().error(f"[Shopify] 订单 {order.get('name', order.get('id'))} fulfillment_orders 为空")
    
    return ""


def _make_sync_meta(sync_batch_id: str) -> Dict[str, object]:
    return {
        "sync_batch_id": sync_batch_id,
        "sync_status": "pending",
        "sync_user": getattr(frappe.session, "user", None) or "Administrator",
        "sync_at": now_datetime(),
    }


def _stringify_payload(data: Dict) -> str:
    return json.dumps(data, ensure_ascii=False)

def _coerce_datetime(value: Optional[str]) -> Optional[str]:
    if not value:
        return None

    attempts = [value]
    if value.endswith("+00:00"):
        attempts.append(value[:-6])
    if "T" in value:
        attempts.append(value.replace("T", " "))

    for candidate in attempts:
        try:
            dt = get_datetime(candidate)
            if dt:
                return dt.strftime("%Y-%m-%d %H:%M:%S")
        except Exception:
            try:
                dt = date_parser.parse(candidate)
                if dt:
                    return dt.strftime("%Y-%m-%d %H:%M:%S")
            except Exception:
                continue
    return None


def _extract_order_fields(shop_name: str, order: Dict, allowed_warehouses: list = None) -> Dict[str, object]:

    shipping = order.get("shippingAddress") or {}

    total_price_info = (
        order.get("totalPriceSet", {}).get("shopMoney", {}) if order else {}
    )

    # 调试：检查 shopify_order_id 的值
    shopify_id = order.get("id")
    
    values: Dict[str, object] = {
        "title": order.get("name") or order.get("id"),
        "custom_shop_name": shop_name,
        "custom_shopify_order_id": shopify_id,
        "raw_payload": _stringify_payload(order),
        "fulfillment_location": fulfillment_location,
    }

    for key, extractor in (
        ("order_number", lambda: order.get("name")),
        ("order_created_at", lambda: _coerce_datetime(order.get("createdAt"))),
        ("destination_country", lambda: shipping.get("countryCodeV2")),
        (
            "total_price",
            lambda: total_price_info.get("amount") if total_price_info else None,
        ),
        (
            "currency",
            lambda: total_price_info.get("currencyCode") if total_price_info else None,
        ),
        (
            "customer_email",
            lambda: (order.get("customer") or {}).get("email"),
        ),
        (
            "customer_name",
            lambda: (order.get("customer") or {}).get("displayName"),
        ),
        (
            "remarks",
            lambda: f"履约仓库: {fulfillment_location}"
            if fulfillment_location
            else "履约仓库: 未指定",
        ),
    ):
        try:
            value = extractor()
        except Exception:
            value = None
        if value not in (None, ""):
            values[key] = value

    return values


def _extract_sku_fields(shop_name: str, product_variant: Dict) -> Dict[str, object]:
    product = product_variant.get("product", {})
    variant = product_variant.get("variant", {})

    # 只导入 ACTIVE 状态的商品，跳过 DRAFT 和 ARCHIVED 状态
    product_status = (product.get("status") or "").upper()
    if product_status not in ["ACTIVE"]:
        return {}

    payload = {
        "product": product,
        "variant": variant,
    }

    values: Dict[str, object] = {
        "title": variant.get("sku") or variant.get("id"),
        "custom_shop_name": shop_name,
        "raw_payload": _stringify_payload(payload),
    }

    # 基础字段提取
    for key, extractor in (
        ("sku_code", lambda: variant.get("sku")),
        ("sku_name", lambda: variant.get("title") or product.get("title")),
        ("sync_status", lambda: product.get("status")),
        ("product_type", lambda: product.get("productType")),
        ("vendor", lambda: product.get("vendor")),
        ("tags", lambda: ", ".join(product.get("tags", [])) if product.get("tags") else None),
        ("description", lambda: product.get("description")),
        ("description_html", lambda: product.get("descriptionHtml")),
        ("total_inventory", lambda: product.get("totalInventory")),
        ("tracks_inventory", lambda: product.get("tracksInventory")),
        ("category_name", lambda: product.get("category", {}).get("name")),
        ("category_full_name", lambda: product.get("category", {}).get("fullName")),
    ):
        try:
            value = extractor()
        except Exception:
            value = None
        if value not in (None, ""):
            values[key] = value

    # 变体字段提取
    for key, extractor in (
        ("variant_id", lambda: variant.get("id")),
        ("variant_title", lambda: variant.get("title")),
        ("price", lambda: float(variant.get("price", 0)) if variant.get("price") else None),
        ("compare_at_price", lambda: float(variant.get("compareAtPrice", 0)) if variant.get("compareAtPrice") else None),
        ("available_for_sale", lambda: variant.get("availableForSale")),
        ("inventory_quantity", lambda: variant.get("inventoryQuantity")),
        ("sellable_online_quantity", lambda: variant.get("sellableOnlineQuantity")),
        ("barcode", lambda: variant.get("barcode")),
        ("taxable", lambda: variant.get("taxable")),
        ("tax_code", lambda: variant.get("taxCode")),
    ):
        try:
            value = extractor()
        except Exception:
            value = None
        if value not in (None, ""):
            values[key] = value

    # 价格范围提取
    price_range = product.get("priceRangeV2", {})
    if price_range:
        min_price = price_range.get("minVariantPrice", {})
        max_price = price_range.get("maxVariantPrice", {})
        
        if min_price.get("amount"):
            values["min_price"] = float(min_price["amount"])
            values["currency_code"] = min_price.get("currencyCode", "USD")
        if max_price.get("amount"):
            values["max_price"] = float(max_price["amount"])

    # 图片信息提取
    images = product.get("images", {}).get("edges", [])
    if images:
        values["image_urls"] = [img["node"]["originalSrc"] for img in images if img["node"].get("originalSrc")]
    
    featured_image = product.get("featuredImage")
    if featured_image and featured_image.get("originalSrc"):
        values["featured_image_url"] = featured_image["originalSrc"]

    return values


def _upsert_doc(doctype: str, key_field: str, data: Dict[str, object]) -> Tuple[str, bool]:
    if not data:
        return "", False

    key_value = data.get(key_field)
    existing_name = None
    if key_value:
        existing_name = frappe.db.get_value(doctype, {key_field: key_value}, "name")

    if existing_name:
        doc = frappe.get_doc(doctype, existing_name)
        doc.update(data)
        doc.save(ignore_permissions=True)
        return doc.name, False

    doc = frappe.new_doc(doctype)
    doc.update(data)
    doc.insert(ignore_permissions=True)
    return doc.name, True


def _upsert_order(shop_name: str, order: Dict, sync_batch_id: str, allowed_warehouses: list = None) -> Tuple[str, bool]:
    values = _extract_order_fields(shop_name, order, allowed_warehouses)
    values.update(_make_sync_meta(sync_batch_id))
    return _upsert_doc("Shopify Order", "custom_shopify_order_id", values)


def _upsert_sku(shop_name: str, product_variant: Dict, sync_batch_id: str) -> Tuple[str, bool]:
    values = _extract_sku_fields(shop_name, product_variant)
    values.update(_make_sync_meta(sync_batch_id))
    return _upsert_doc("Shopify SKU", "sku_code", values)


def _build_order_query(since: Optional[str] = None) -> Optional[str]:
    if not since:
        return None
    # Shopify GraphQL requires ISO8601 format; caller should pass ISO string.
    return f"created_at:>={since}"


def _build_sku_query(since: Optional[str] = None) -> Optional[str]:
    if not since:
        # 如果没有指定时间，使用默认的查询条件获取所有商品
        return "updated_at:>=2000-01-01T00:00:00Z"
    return f"updated_at:>={since}"


def _ensure_item_group_exists(product_type: str, parent_group: str = "All Item Groups") -> str:
    """确保 Item Group 存在，如果不存在则创建"""
    if not product_type:
        return parent_group
    
    # 检查 Item Group 是否已存在
    existing_group = frappe.db.get_value("Item Group", {"item_group_name": product_type})
    if existing_group:
        return existing_group
    
    try:
        # 创建新的 Item Group
        item_group_doc = frappe.new_doc("Item Group")
        item_group_doc.item_group_name = product_type
        item_group_doc.parent_item_group = parent_group
        item_group_doc.is_group = 0
        item_group_doc.insert(ignore_permissions=True)
                
        return product_type
        
    except Exception as e:
        frappe.logger().error(f"[Shopify] 创建 Item Group 失败: {product_type}, 错误: {str(e)}")
        # 如果创建失败，返回父组
        return parent_group


def _ensure_unit_exists(unit_name: str) -> bool:
    """
    确保UOM单位存在，如果不存在则创建
    """
    if not unit_name:
        return False
    
    # 检查UOM是否已存在
    existing_uom = frappe.db.get_value("UOM", {"uom_name": unit_name})
    if existing_uom:
        return True
    
    try:
        # 创建新的UOM
        uom_doc = frappe.new_doc("UOM")
        uom_doc.uom_name = unit_name
        uom_doc.must_be_whole_number = 1  # 默认为整数
        uom_doc.insert(ignore_permissions=True)
        
        frappe.logger().info(f"[SKU映射] 创建新的UOM单位: {unit_name}")
        return True
        
    except Exception as e:
        frappe.logger().error(f"[SKU映射] 创建UOM单位失败: {unit_name}, 错误: {str(e)}")
        return False


@frappe.whitelist()
def generate_standard_names(sku_code: str, variant_title: str, base_chinese_name: str) -> str:
    """
    根据SKU、变体标题和基础中文名称生成标准中文名称
    格式：产品名称+颜色+规格
    """
    if not base_chinese_name or not variant_title:
        return base_chinese_name
    
    # 颜色映射
    color_mapping = {
        "black": "黑色", "blk": "黑色", "blkl": "黑色", "blkm": "黑色", "blks": "黑色",
        "white": "白色", "wht": "白色", "snow": "白色", "snol": "白色", "snom": "白色", "snos": "白色",
        "blue": "蓝色", "blu": "蓝色", "sto": "蓝色", "stormy": "蓝色",
        "stone": "石色", "stn": "石色", "stnl": "石色", "stnm": "石色", "stns": "石色",
        "gray": "灰色", "grey": "灰色", "gry": "灰色",
        "red": "红色", "rd": "红色",
        "green": "绿色", "grn": "绿色",
        "brown": "棕色", "brn": "棕色",
        "pink": "粉色", "pnk": "粉色",
        "purple": "紫色", "prp": "紫色",
        "yellow": "黄色", "ylw": "黄色",
        "orange": "橙色", "org": "橙色"
    }
    
    # 规格映射
    size_mapping = {
        "s": "S号", "small": "S号",
        "m": "M号", "medium": "M号", 
        "l": "L号", "large": "L号",
        "xl": "XL号", "x-large": "XL号",
        "xxl": "XXL号", "xx-large": "XXL号",
        "xs": "XS号", "x-small": "XS号"
    }
    
    # 解析变体标题，格式通常是 "颜色 / 规格" 或 "颜色 / 规格"
    variant_parts = variant_title.split(" / ")
    if len(variant_parts) != 2:
        return base_chinese_name
    
    color_part = variant_parts[0].lower().strip()
    size_part = variant_parts[1].lower().strip()
    
    # 查找颜色
    chinese_color = ""
    for key, value in color_mapping.items():
        if key in color_part:
            chinese_color = value
            break
    
    # 查找规格
    chinese_size = ""
    for key, value in size_mapping.items():
        if key in size_part:
            chinese_size = value
            break
    
    # 组合最终名称
    result = base_chinese_name
    if chinese_color:
        result += chinese_color
    if chinese_size:
        result += chinese_size
    
    return result


def get_local_unit_weight(sku_code: str, variant_title: str = "") -> dict:
    """
    根据SKU代码获取中文名称、单位和重量
    严格按照SKU前缀字符进行匹配
    """
    if not sku_code:
        return {
            "chinese_name": "",
            "unit": "PCS",
            "weight": 0.10
        }
    
    # SKU前缀映射表（严格按照前缀字符）
    # 注意：unit使用系统中已存在的中文单位
    sku_mapping = {
        # 根据 Paire.ini 完整数据生成的 SKU 前缀映射表
        "MSAS01": {
            "chinese_name": "SilkMode 踝袜",
            "unit": "Pair",
            "weight": 0.032
        },
        "MSNS01": {
            "chinese_name": "SilkMode 隐形袜",
            "unit": "Pair",
            "weight": 0.031
        },
        "MSQS01": {
            "chinese_name": "SilkMode 四分袜",
            "unit": "Pair",
            "weight": 0.048
        },
        "WUGS12": {
            "chinese_name": "BioTracker 丁字裤",
            "unit": "PCS",
            "weight": 0.030
        },
        "AS01": {
            "chinese_name": "踝袜",
            "unit": "Pair",
            "weight": 0.055
        },
        "WUBS12": {
            "chinese_name": "BioTracker 女士平角裤",
            "unit": "PCS",
            "weight": 0.030
        },
        "GA01": {
            "chinese_name": "防滑踝袜",
            "unit": "Pair",
            "weight": 0.060
        },
        "WUHH12": {
            "chinese_name": "BioTracker 高腰全包内裤",
            "unit": "PCS",
            "weight": 0.030
        },
        "CCQS01": {
            "chinese_name": "舒适俱乐部四分袜",
            "unit": "Pair",
            "weight": 0.050
        },
        "QS01": {
            "chinese_name": "四分袜",
            "unit": "Pair",
            "weight": 0.060
        },
        "WULM12": {
            "chinese_name": "BioTracker 经典比基尼内裤",
            "unit": "PCS",
            "weight": 0.030
        },
        "WUHC12": {
            "chinese_name": "BioTracker 高腰高腿内裤",
            "unit": "PCS",
            "weight": 0.030
        },
        "WUMH12": {
            "chinese_name": "BioTracker 中腰内裤",
            "unit": "PCS",
            "weight": 0.030
        },
        "NS01": {
            "chinese_name": "隐形袜",
            "unit": "Pair",
            "weight": 0.045
        },
        "AA01": {
            "chinese_name": "运动款踝袜",
            "unit": "Pair",
            "weight": 0.050
        },
        "CS01": {
            "chinese_name": "小腿袜",
            "unit": "Pair",
            "weight": 0.070
        },
        "AC01": {
            "chinese_name": "读书会中筒袜",
            "unit": "Pair",
            "weight": 0.070
        },
        "GC01": {
            "chinese_name": "防滑中筒袜",
            "unit": "Pair",
            "weight": 0.072
        },
        "CS02": {
            "chinese_name": "松口小腿袜",
            "unit": "Pair",
            "weight": 0.065
        },
        "OGBX": {
            "chinese_name": "随行男士平角内裤",
            "unit": "PCS",
            "weight": 0.080
        },
        "MUBX12": {
            "chinese_name": "BioTracker 男士平角内裤",
            "unit": "PCS",
            "weight": 0.100
        },
        "AQ01": {
            "chinese_name": "运动款四分袜",
            "unit": "Pair",
            "weight": 0.045
        },
        "QSBC": {
            "chinese_name": "植物系列四分袜",
            "unit": "Pair",
            "weight": 0.060
        },
        "CP01": {
            "chinese_name": "压缩袜",
            "unit": "Pair",
            "weight": 0.085
        },
        "WS01": {
            "chinese_name": "雪地运动袜",
            "unit": "Pair",
            "weight": 0.095
        },
        "BP02": {
            "chinese_name": "羊绒美利奴螺纹中筒袜",
            "unit": "Pair",
            "weight": 0.070
        },
        "OGTK11": {
            "chinese_name": "随行旅行背心",
            "unit": "PCS",
            "weight": 0.145
        },
        "BP01": {
            "chinese_name": "羊绒美利奴中筒袜",
            "unit": "Pair",
            "weight": 0.070
        },
        "WCWT22": {
            "chinese_name": "女士 ReBrew 经典 T 恤",
            "unit": "PCS",
            "weight": 0.140
        },
        "WRPW41": {
            "chinese_name": "女士 ReFlex 直筒裤",
            "unit": "PCS",
            "weight": 0.390
        },
        "GTCT22": {
            "chinese_name": "GlowTint 旅行内衣上衣",
            "unit": "PCS",
            "weight": 0.182
        },
        "IUMH12": {
            "chinese_name": "Lipstick 经典比基尼内裤（三条装）",
            "unit": "PCS",
            "weight": 0.130
        },
        "MCWT22": {
            "chinese_name": "男士 ReBrew 经典 T 恤",
            "unit": "PCS",
            "weight": 0.180
        },
        "IUHW12": {
            "chinese_name": "Lipstick 高腰内裤（三条装）",
            "unit": "PCS",
            "weight": 0.140
        },
        "WCBT22": {
            "chinese_name": "女士有机棉螺纹背心",
            "unit": "PCS",
            "weight": 0.150
        },
        "WCBS22": {
            "chinese_name": "女士有机棉螺纹 T 恤",
            "unit": "PCS",
            "weight": 0.165
        },
        "WFTT11": {
            "chinese_name": "基础款保暖长袖",
            "unit": "PCS",
            "weight": 0.125
        },
        "GBRA22": {
            "chinese_name": "GlowTint 旅行内衣",
            "unit": "PCS",
            "weight": 0.160
        },
        "WFTL11": {
            "chinese_name": "基础款保暖打底裤",
            "unit": "PCS",
            "weight": 0.125
        },
        "LSBR42": {
            "chinese_name": "女士 BioFlex 交叉背轻支撑内衣",
            "unit": "PCS",
            "weight": 0.130
        },
        "06DS42": {
            "chinese_name": "女士 BioFlex 高腰短裤",
            "unit": "PCS",
            "weight": 0.100
        },
        "06PS42": {
            "chinese_name": "女士 BioFlex 高腰口袋短裤",
            "unit": "PCS",
            "weight": 0.100
        },
        "MSBR42": {
            "chinese_name": "女士 BioFlex 交叉背中支撑运动内衣",
            "unit": "PCS",
            "weight": 0.130
        },
        "CT21": {
            "chinese_name": "男女通用 BreezeBlend 经典 T 恤",
            "unit": "PCS",
            "weight": 0.230
        },
        "25PL41": {
            "chinese_name": "女士 BioFlex 25英寸口袋打底裤",
            "unit": "PCS",
            "weight": 0.200
        },
        "MRPS41": {
            "chinese_name": "男士 ReFlex 修身裤",
            "unit": "PCS",
            "weight": 0.405
        },
        "LSHT21": {
            "chinese_name": "男女通用 BreezeBlend 长袖 T 恤",
            "unit": "PCS",
            "weight": 0.170
        },
        "WMSS21": {
            "chinese_name": "女士 100% 美利奴羊毛短袖 T 恤",
            "unit": "PCS",
            "weight": 0.190
        },
        "WMLS21": {
            "chinese_name": "女士 100% 美利奴羊毛长袖 T 恤",
            "unit": "PCS",
            "weight": 0.230
        },
        "MMSS21": {
            "chinese_name": "男士 100% 美利奴羊毛短袖 T 恤",
            "unit": "PCS",
            "weight": 0.190
        },
        "MMLS21": {
            "chinese_name": "男士 100% 美利奴羊毛长袖 T 恤",
            "unit": "PCS",
            "weight": 0.230
        },
        "SCSP31": {
            "chinese_name": "男女通用 CoolBlend 直筒裤",
            "unit": "PCS",
            "weight": 0.700
        },
        "SCQZ31": {
            "chinese_name": "男女通用 CoolBlend 拉链领针织衫",
            "unit": "PCS",
            "weight": 0.800
        },
        "SWCN31": {
            "chinese_name": "男女通用 SilKnit 圆领毛衣",
            "unit": "PCS",
            "weight": 0.600
        },
        "WCNE21": {
            "chinese_name": "女士 BreezeBlend 圆领 T 恤",
            "unit": "PCS",
            "weight": 0.180
        },
        "IUFP12": {
            "chinese_name": "Lipstick 高腰内裤（七条装）",
            "unit": "PCS",
            "weight": 0.225
        },

        "WLHT21": {
            "chinese_name": "女士 BreezeBlend 长袖 T 恤",
            "unit": "PCS",
            "weight": 0.200
        },
        "IUBP12": {
            "chinese_name": "Lipstick 经典比基尼内裤（七条装）",
            "unit": "PCS",
            "weight": 0.215
        },
        "MGFS41": {
            "chinese_name": "男士 GoFlex 运动短裤",
            "unit": "PCS",
            "weight": 0.400
        },
        "WGFS41": {
            "chinese_name": "女士 GoFlex 运动短裤",
            "unit": "PCS",
            "weight": 0.400
        },
        
        # 注意：前缀必须完全匹配，区分大小写
        # 根据 Paire.ini 完整数据生成的映射表，共67个唯一前缀
    }
    
    # 提取SKU前缀（第一个分割线前的部分）
    sku_prefix = sku_code.split('-')[0] if '-' in sku_code else sku_code
    
    # 严格匹配SKU前缀
    if sku_prefix in sku_mapping:
        mapping_result = sku_mapping[sku_prefix]
        
        # 确保单位在系统中存在
        unit_name = mapping_result["unit"]
        if _ensure_unit_exists(unit_name):
            # 如果有变体标题，生成完整的中文名称
            if variant_title and mapping_result["chinese_name"]:
                mapping_result["chinese_name"] = generate_standard_names(
                    sku_code, variant_title, mapping_result["chinese_name"]
                )
            return mapping_result
        else:
            # 如果单位创建失败，使用默认单位
            result = {
                "chinese_name": mapping_result["chinese_name"],
                "unit": "Nos",
                "weight": mapping_result["weight"]
            }
            # 如果有变体标题，生成完整的中文名称
            if variant_title and result["chinese_name"]:
                result["chinese_name"] = generate_standard_names(
                    sku_code, variant_title, result["chinese_name"]
                )
            return result
    
    # 如果没有找到匹配，返回默认值
    base_result = {
        "chinese_name": "",
        "unit": "PCS",
        "weight": 0.10
    }

    return base_result


def _ensure_brand_exists(brand_name: str) -> str:
    """确保 Brand 存在，如果不存在则创建"""
    if not brand_name:
        return None
    
    # 检查 Brand 是否已存在
    existing_brand = frappe.db.get_value("Brand", {"brand": brand_name})
    if existing_brand:
        return brand_name
    
    try:
        # 创建新的 Brand
        brand_doc = frappe.new_doc("Brand")
        brand_doc.brand = brand_name
        brand_doc.insert(ignore_permissions=True)
                
        return brand_name
        
    except Exception as e:
        frappe.logger().error(f"[Shopify] 创建 Brand 失败: {brand_name}, 错误: {str(e)}")
        # 如果创建失败，返回原名称
        return brand_name


@frappe.whitelist()
def sync_shopify_orders(
    shop_name: str,
    batch_size: int = DEFAULT_ORDER_BATCH_SIZE,
    cursor: Optional[str] = None,
    since: Optional[str] = None,
    max_pages: int = DEFAULT_ORDER_MAX_PAGES,
) -> Dict[str, object]:
    """同步Shopify订单到 staging 表。

    Args:
        shop_name: usershop.name
        batch_size: 每次请求数量，最大 50
        cursor: Shopify GraphQL 游标，用于增量翻页
        since: ISO8601 字符串，限定创建时间下限
        max_pages: 允许抓取的最大分页数，避免全量时长时间阻塞
    """

    frappe.logger().error(
        "[Shopify][Orders] 请求开始: shop=%s, batch_size=%s, cursor=%s, since=%s, max_pages=%s",
        shop_name,
        batch_size,
        cursor,
        since,
        max_pages,
    )

    fetched = 0
    created = 0
    updated = 0
    skipped_warehouse = 0
    skipped_draft = 0
    sync_batch_id = _make_sync_batch_id()

    try:
        client = ShopifyClient(shop_name)
    except ShopifyClientError as exc:
        frappe.logger().error(f"Shopify配置错误: {exc}")
        return {"success": False, "message": str(exc)}

    # 一次性获取允许的仓库列表，避免在循环中重复查询
    allowed_warehouses = _get_allowed_warehouses(shop_name)
    # 强制要求仓库过滤必须配置
    if not allowed_warehouses:
        frappe.throw(
            msg=f"店铺 {shop_name} 未配置 allowed_warehouses（JSON 数组），无法同步订单。",
            exc=frappe.ValidationError,
        )

    batch_size = cint(batch_size) or DEFAULT_ORDER_BATCH_SIZE
    max_pages = cint(max_pages) or DEFAULT_ORDER_MAX_PAGES

    effective_since = since or DEFAULT_ORDER_SINCE
    query = _build_order_query(effective_since)
    current_cursor = cursor
    has_next = True
    pages = 0

    try:
        while has_next and pages < max_pages:
            orders, next_cursor, has_next_page = client.fetch_orders(
                first=min(batch_size, 250),
                after=current_cursor,
                query=query,
            )

            if not orders:
                break

            for order in orders:
                # 主循环内做过滤：跳过草稿与不在允许仓库范围内的订单
                status = (order.get("status") or "").upper()
                if status == "DRAFT":
                    skipped_draft += 1
                    continue

                order_locations = _get_fulfillment_locations(order)
                if allowed_warehouses:
                    if not order_locations or not (set(allowed_warehouses) & order_locations):
                        skipped_warehouse += 1
                        continue
                
                name, is_created = _upsert_order(shop_name, order, sync_batch_id, allowed_warehouses)

                frappe.logger().info(
                    "[Shopify][Orders] 同步订单: %s (created=%s)",
                    name,
                    is_created,
                )

                fetched += 1
                if is_created:
                    created += 1
                else:
                    updated += 1

            current_cursor = next_cursor
            has_next = bool(has_next_page and next_cursor)
            pages += 1

        frappe.db.commit()

        message_parts = [_("同步订单完成: {0} 条, 新增 {1} 条, 更新 {2} 条").format(fetched, created, updated)]
        if skipped_warehouse > 0:
            message_parts.append(_("跳过仓库过滤: {0} 条").format(skipped_warehouse))
        if skipped_draft > 0:
            message_parts.append(_("跳过草稿订单: {0} 条").format(skipped_draft))
        
        return {
            "success": True,
            "message": " | ".join(message_parts),
            "fetched": fetched,
            "created": created,
            "updated": updated,
            "skipped_warehouse": skipped_warehouse,
            "skipped_draft": skipped_draft,
            "has_next": has_next,
            "next_cursor": current_cursor,
            "sync_batch_id": sync_batch_id,
        }

    except ShopifyClientError as exc:
        frappe.db.rollback()
        frappe.logger().error(
            "[Shopify][Orders] 异常: %s\n%s",
            exc,
            frappe.get_traceback(),
        )
        frappe.logger().error(f"Shopify订单同步失败: {frappe.get_traceback()}")
        return {
            "success": False,
            "message": str(exc),
            "error": str(exc),
        }
    except Exception as exc:
        frappe.db.rollback()
        frappe.logger().error(
            "[Shopify][Orders] 未知异常: %s\n%s",
            exc,
            frappe.get_traceback(),
        )
        frappe.logger().error(f"Shopify订单同步失败: {frappe.get_traceback()}")
        return {
            "success": False,
            "message": str(exc),
            "error": str(exc),
        }

@frappe.whitelist()
def sync_and_import_shopify_skus(shop_name, parent_item_group=None, default_company=None, default_warehouse=None, default_supplier=None, batch_size=50, max_pages=1, since=None):
    """
    直接从 Shopify GraphQL 获取数据并导入到 ERPNext Item 表
    不经过 Shopify SKU staging 表，一步完成同步+导入
    """
    try:
        # 记录开始日志
        frappe.logger().info(
            f"[直接导入] 开始执行 - 店铺: {shop_name}, 父组: {parent_item_group}, 公司: {default_company}, 仓库: {default_warehouse}, 批次: {batch_size}, 页数: {max_pages}, 时间: {since}"
        )
        
        # 验证父目录组是否存在且是分组
        try:
            parent_group_doc = frappe.get_doc("Item Group", parent_item_group)
            if not parent_group_doc.is_group:
                return {
                    "success": False,
                    "error": f"父目录组 '{parent_item_group}' 不是分组，请选择 is_group=1 的 Item Group"
                }
        except Exception as e:
            return {
                "success": False,
                "error": f"父目录组验证失败: {str(e)}"
            }
        

        client = ShopifyClient(shop_name)
    
        
        batch_size = cint(batch_size) or 50
        max_pages = cint(max_pages) if max_pages else None
        effective_since = since or "2000-01-01T00:00:00Z"
        query = f"updated_at:>={effective_since}"
        

        
        # 直接从 GraphQL 获取数据并处理
        created = 0
        updated = 0
        skipped = 0
        skipped_non_saleable = 0
        barcode_conflicts = 0
        errors = []
        current_cursor = None
        has_next = True
        pages = 0

        while has_next and (max_pages is None or pages < max_pages):
            pages += 1
            
            # 从 Shopify 获取数据
            skus, next_cursor, has_next_page = client.fetch_skus(
                first=min(batch_size, 50),
                after=current_cursor,
                query=query,
            )
            

            if not skus:
                break

            # 处理每个变体（entry 是变体级别的数据）
            for entry in skus:
                try:
                    # 获取变体SKU
                    variant_sku = entry.get('variant', {}).get('sku', '')
                    
                    
                    # 检查商品状态，只处理ACTIVE状态的商品
                    product = entry.get("product", {})
                    product_status = (product.get("status") or "").upper()
                    
                    if product_status != "ACTIVE":
                        skipped += 1
                        continue
                    
                    # 直接处理变体并创建/更新Item
                    result = _process_shopify_product_direct(
                        entry, parent_item_group, default_company, 
                        default_warehouse, default_supplier
                    )
                    
                    created += result.get("created", 0)
                    updated += result.get("updated", 0)
                    skipped_non_saleable += result.get("skipped_non_saleable", 0)
                    barcode_conflicts += result.get("barcode_conflicts", 0)
                    

                except Exception as e:
                    variant_sku = entry.get('variant', {}).get('sku', '未知SKU')
                    error_msg = f"处理商品失败: {str(e)}"
                    frappe.logger().error(f"[主循环异常] SKU: {variant_sku}, 错误: {str(e)}, JSON: {json.dumps(entry, ensure_ascii=False, indent=2)[:1000]}...")
                    errors.append(error_msg)

            current_cursor = next_cursor
            has_next = has_next_page
        
        # 返回结果
        total_processed = created + updated
        message = f"直接导入完成：处理 {total_processed} 个变体，创建 {created} 个，更新 {updated} 个，跳过 {skipped} 个"
        
        # 记录最终统计
        frappe.logger().info(f"[统计] 最终结果: 处理={total_processed}, 创建={created}, 更新={updated}, 跳过={skipped}, 不可销售={skipped_non_saleable}")
        
        if skipped_non_saleable > 0:
            message += f"，跳过不可销售 {skipped_non_saleable} 个"
        
        if barcode_conflicts > 0:
            message += f"，条码冲突 {barcode_conflicts} 个"
        
        if errors:
            message += f"，失败 {len(errors)} 个"
        
        frappe.logger().error(f"[直接导入] 完成 - {message}")

        return {
            "success": len(errors) == 0,
            "message": message,
            "created": created,
            "updated": updated,
            "skipped": skipped,
            "skipped_non_saleable": skipped_non_saleable,
            "barcode_conflicts": barcode_conflicts,
            "errors": errors
        }
        
    except Exception as e:
        error_msg = f"直接导入失败: {str(e)}"
        frappe.logger().error(f"[直接导入] 异常错误 - {error_msg}")
        return {
            "success": False,
            "error": error_msg
        }


def _process_shopify_product_direct(entry, parent_item_group, default_company, default_warehouse, default_supplier):
    """
    直接处理 Shopify 商品数据并创建/更新 Item
    不经过 Shopify SKU staging 表
    """
    try:
        product = entry.get("product", {})
        variant = entry.get("variant", {})
        
        # entry 已经是一个变体，不需要循环
        variant_node = variant
        sku_code = variant_node.get("sku")
        
        
        if not sku_code:
            return {
                "created": 0,
                "updated": 0,
                "skipped_non_saleable": 0,
                "barcode_conflicts": 0
            }
        
        # 检查是否已存在
        existing_item = frappe.db.get_value("Item", {"item_code": sku_code})
        
        if existing_item:
            item = frappe.get_doc("Item", existing_item)
            updated = 1
            created = 0
        else:
            item = frappe.new_doc("Item")
            item.item_code = sku_code
            created = 1
            updated = 0

        # 设置基础信息
        item.flags.ignore_permissions = True
        
        # 商品名称处理
        product_title = product.get("title") or ""
        variant_title = variant_node.get("title") or ""
        
        # 检查是否有多个变体（通过检查product.variants.edges长度）
        variants_count = len((product.get("variants") or {}).get("edges", []))
        if variants_count > 1 and product_title:
            item.item_name = f"{product_title} - {variant_title}"
        else:
            item.item_name = variant_title or product_title or sku_code
        
        item.item_name = item.item_name.strip()
        

        item.stock_uom = "PCS"
        
        # 使用SKU前缀智能设置重量
        local_info = get_local_unit_weight(sku_code, variant_title)
        

        # 设置中文名称和重量
        if local_info.get("weight", 0) > 0:
            item.weight_per_unit = local_info["weight"]
        if local_info.get("chinese_name"):
            item.custom_item_name_cn = local_info["chinese_name"]
        if local_info.get("unit"):
            item.stock_uom = local_info["unit"]
        item.disabled = 0

        # 基础商品信息
        item.is_stock_item = 1
        item.is_sales_item = 1
        item.is_purchase_item = 1
        
        # 产品类型 - 自动维护 Item Group
        product_type = product.get("productType")
        if product_type:
            item_group_name = _ensure_item_group_exists(product_type, parent_item_group)
            item.item_group = item_group_name
        else:
            item.item_group = parent_item_group
        
        # 品牌信息
        vendor = product.get("vendor")
        if vendor:
            brand_name = _ensure_brand_exists(vendor)
            item.brand = brand_name
            
            # 添加供应商信息到 Supplier Items 子表
            if default_supplier:
                # 检查是否已存在该供应商
                existing_supplier = False
                if hasattr(item, 'supplier_items'):
                    for supplier_item in item.supplier_items:
                        if supplier_item.supplier == default_supplier:
                            existing_supplier = True
                            break
                
                if not existing_supplier:
                    supplier_item = item.append("supplier_items")
                    supplier_item.supplier = default_supplier
                    supplier_item.supplier_part_no = sku_code
        
        # 描述信息
        description = product.get("description")
        if description:
            item.description = description
        
        # 价格信息 - 保存销售价格和成本价格
        price = variant_node.get("price")
        if price:
            try:
                item.valuation_rate = float(price)*4.5*0.2
                item.custom_sales_priceusd = float(price)*1.5
            except (ValueError, TypeError):
                pass
        
        # 货币信息 - 从价格范围获取货币代码
        price_range = product.get("priceRangeV2", {})
        if price_range:
            min_price = price_range.get("minVariantPrice", {})
            currency_code = min_price.get("currencyCode")
            if currency_code and hasattr(item, 'currency'):
                item.currency = currency_code
        
        # 条码信息 - 只在Item不存在时添加条码
        if not existing_item:
            if not item.barcodes:
                item.barcodes = []
            barcode_row = item.append("barcodes")
            barcode_row.barcode = sku_code
        
        # 库存跟踪设置
        tracks_inventory = product.get("tracksInventory", True)
        if not tracks_inventory:
            item.is_stock_item = 0

        
        # 图片信息
        featured_image = product.get("featuredImage")
        if featured_image and featured_image.get("originalSrc"):
            item.image = featured_image["originalSrc"]
        
        # 保存 Shopify 原始数据到 custom_shop_rawdate 字段
        if hasattr(item, "custom_shop_rawdate"):
            item.custom_shop_rawdate = json.dumps(entry, ensure_ascii=False, indent=2)
        
        # 保存 Item
        try:
            item.save()
        except Exception as save_error:
            frappe.logger().error(f"[Item保存失败] SKU: {sku_code}, 错误: {str(save_error)}, Item内容: {json.dumps(item.as_dict(), ensure_ascii=False, indent=2)[:1000]}...")
            raise
        
        # 创建 Item Defaults 记录
        if default_company and default_warehouse:
            try:
                # 先删除其他公司的Item Default记录，确保只有当前公司的记录
                frappe.db.delete("Item Default", {
                    "parent": sku_code,
                    "parentfield": "item_defaults",
                    "parenttype": "Item",
                    "company": ["!=", default_company]
                })
                
                # 检查当前公司的Item Default是否存在
                existing_default = frappe.db.exists("Item Default", {
                    "parent": sku_code,
                    "parentfield": "item_defaults",
                    "parenttype": "Item",
                    "company": default_company
                })
                
                if not existing_default:
                    item_default = frappe.new_doc("Item Default")
                    item_default.parent = sku_code
                    item_default.parentfield = "item_defaults"
                    item_default.parenttype = "Item"
                    item_default.company = default_company
                    item_default.default_warehouse = default_warehouse
                    item_default.insert(ignore_permissions=True)
                    
            except Exception as e:
                frappe.logger().error(f"创建 Item Default 失败: {str(e)}")

        return {
            "created": created,
            "updated": updated,
            "skipped_non_saleable": 0,
            "barcode_conflicts": 0
        }
        
    except Exception as e:
        sku_code = entry.get("variant", {}).get("sku", "未知SKU")
        frappe.logger().error(f"[处理商品异常] SKU: {sku_code}, 错误: {str(e)}, JSON: {json.dumps(entry, ensure_ascii=False, indent=2)[:1000]}...")
        return {
            "created": 0,
            "updated": 0,
            "skipped_non_saleable": 0,
            "barcode_conflicts": 0
        }


@frappe.whitelist()
def import_shopify_orders_to_erp(
    order_names,
    warehouse=None,
    auto_dispatch=False,
    maintain_customer=False,
):
    if isinstance(order_names, str):
        try:
            order_names = json.loads(order_names)
        except Exception:
            order_names = [order_names]

    order_names = [name for name in order_names if name]

    if not order_names:
        return {
            "success": False,
            "message": "Please select Shopify Orders to import",
        }

    if not warehouse:
        return {
            "success": False,
            "message": "Please choose the target warehouse",
        }

    if not frappe.db.exists("Warehouse", warehouse):
        return {
            "success": False,
            "message": frappe._("Warehouse {0} not found").format(warehouse),
        }

    auto_dispatch_flag = bool(cint(auto_dispatch))

    frappe.logger().info(
        "[Shopify][Orders] Import request: orders=%s, warehouse=%s, auto_dispatch=%s",
        order_names,
        warehouse,
        auto_dispatch_flag,
    )

    created = 0
    skipped = 0
    processed_orders = []
    errors = []

    for order_docname in order_names:
        try:
            order_doc = frappe.get_doc("Shopify Order", order_docname)
        except frappe.DoesNotExistError:
            errors.append(f"{order_docname}: Shopify Order not found")
            break

        if getattr(order_doc, "sync_status", "").lower() == "imported":
            skipped += 1
            continue

        try:
            frappe.db.begin()
            sales_order_name = _import_single_order_to_erp(
                order_doc=order_doc,
                warehouse=warehouse,
                auto_dispatch=auto_dispatch_flag,
                maintain_customer=False,
                allowed_locations=set(allowed_warehouses),
            )
            created += 1
            processed_orders.append(sales_order_name)
            frappe.db.commit()
        except MissingSKUError as exc:
            frappe.db.rollback()
            message = f"{order_docname}: {exc}"
            errors.append(message)
            frappe.logger().error("[Shopify][Orders] Import aborted due to missing SKU: %s", message)
            break
        except Exception as exc:  # noqa: BLE001
            frappe.db.rollback()
            frappe.logger().error(
                "[Shopify][Orders] Import failed for %s: %s\n%s",
                order_docname,
                exc,
                frappe.get_traceback(),
            )
            errors.append(f"{order_docname}: {exc}")
            break

    success = len(errors) == 0

    summary = [_("Imported {0} orders").format(created)]
    if skipped:
        summary.append(_("Skipped {0} already imported orders").format(skipped))
    if errors:
        summary.append(_("Stopped due to errors"))

    message = " | ".join(summary)
    if errors:
        frappe.logger().error(
            "[Shopify][Orders] Import stopped. created=%s skipped=%s errors=%s",
            created,
            skipped,
            errors,
        )
        error_message = "\n".join(errors)
        message = f"{message}\n{error_message}"
        frappe.logger().error(f"Shopify Order Import Errors - Orders: {order_names}\nWarehouse: {warehouse}\nErrors:\n{error_message}")
    else:
        frappe.logger().info(
            "[Shopify][Orders] Import completed. created=%s skipped=%s",
            created,
            skipped,
        )

    return {
        "success": success,
        "message": message,
        "created": created,
        "skipped": skipped,
        "orders": processed_orders,
        "errors": errors,
        "maintain_customer": False,
    }


def _import_single_order_to_erp(
    *, order_doc, warehouse: str, auto_dispatch: bool, maintain_customer: bool, allowed_locations: set
) -> str:
    payload = json.loads(order_doc.raw_payload or "{}")

    if not payload:
        raise ValueError("Order payload is empty")

    line_items = (payload.get("lineItems") or {}).get("nodes") or []
    if not line_items:
        raise ValueError("Order has no line items")

    missing_skus = []
    item_rows = []
    # 仅导入我们能发货的行：根据 FO 地点过滤
    try:
        # allowed_locations 已由上层计算并传入
        location_map = _get_line_item_location_map(payload)

    except Exception as e:
        frappe.logger().error(
            f"[Shopify][Orders] Import failed for order {getattr(order_doc, 'name', 'unknown')}: {str(e)}"
        )
        raise

    filtered_lines = []
    has_excluded = False
    for line in line_items:
        line_id = line.get("id")
        loc_name = location_map.get(str(line_id))
        if loc_name and (loc_name in allowed_locations):
            filtered_lines.append(line)
        else:
            has_excluded = True

    # 如果有被排除的行，标记多仓提示
    if has_excluded:
        _set_optional_field(sales_order, "custom_has_multi_warehouse", 1)

    # 仅解析保留的行
    # 1) 收集唯一 SKU
    sku_set = set()
    for line in filtered_lines:
        sku_val = line.get("sku")
        if sku_val is None:
            missing_skus.append(f"Line without SKU (null): {line.get('name')}")
            continue
        sku_str = str(sku_val).strip()
        if not sku_str:
            missing_skus.append(f"Line without SKU (empty): {line.get('name')}")
            continue
        sku_set.add(sku_str)

    # 2) 批量查询 Item 基本信息并建立映射
    item_code_to_uom = {}
    if sku_set:
        items = frappe.get_all(
            "Item",
            filters={"item_code": ["in", list(sku_set)]},
            fields=["item_code", "stock_uom"],
            limit_page_length=0,
        )
        for it in items:
            item_code_to_uom[str(it.get("item_code"))] = it.get("stock_uom") or "Nos"

    # 将未命中的 SKU 记为缺失
    for sku in sku_set:
        if sku not in item_code_to_uom:
            missing_skus.append(sku)

    # 3) 生成行项（仅对命中的 SKU）
    for line in filtered_lines:
        sku_str = str(line.get("sku") or "").strip()
        if not sku_str or sku_str not in item_code_to_uom:
            continue

        qty = flt(line.get("quantity") or 0)
        if qty <= 0:
            missing_skus.append(f"SKU {sku_str} has non-positive quantity")
            continue

        item_rows.append(
            {
                "item_code": sku_str,
                "item_name": line.get("title") or line.get("name") or sku_str,
                "description": line.get("name") or line.get("title") or sku_str,
                "qty": qty,
                "uom": item_code_to_uom.get(sku_str) or "Nos",
                # 添加Shopify相关字段
                "custom_shopify_line_item_id": line.get("id"),
                "custom_shopify_variant_id": (line.get("variant", {}) or {}).get("id"),
                "custom_shopify_product_id": ((line.get("variant", {}) or {}).get("product", {}) or {}).get("id"),
            }
        )

    if missing_skus:
        raise MissingSKUError(
            frappe._("Missing or invalid SKUs: {0}").format(", ".join(missing_skus))
        )

    company = _get_order_company(order_doc.shop_name)
    customer_name = "Shopify Default Customer"  # 现在需要在customer默认新增一个shopify的customer

    transaction_date = _coerce_datetime(payload.get("createdAt"))
    transaction_date = getdate(transaction_date) if transaction_date else getdate(now_datetime())

    currency = payload.get("currencyCode") or frappe.db.get_default("currency")
    if not currency:
        currency = frappe.db.get_value("Company", company, "default_currency")

    sales_order = frappe.new_doc("Sales Order")
    sales_order.flags.ignore_permissions = True
    sales_order.flags.shopify_payload = payload
    sales_order.company = company
    sales_order.customer = customer_name
    sales_order.currency = currency
    sales_order.conversion_rate = 1
    sales_order.transaction_date = transaction_date
    sales_order.delivery_date = transaction_date
    sales_order.po_no = payload.get("name") or payload.get("id")
    remarks_parts = []
    if payload.get("note"):
        remarks_parts.append(payload["note"].strip())

    merchant_notes = []
    if payload.get("merchantEditable"):
        merchant_notes.append(str(payload.get("merchantEditable")).strip())
    if payload.get("additionalNote"):
        merchant_notes.append(str(payload.get("additionalNote")).strip())
    if merchant_notes:
        remarks_parts.append("Merchant Notes: " + " | ".join(note for note in merchant_notes if note))

    tip_details = payload.get("tipPaymentGateway")
    if isinstance(tip_details, str) and tip_details.strip():
        remarks_parts.append(f"Tip via: {tip_details.strip()}")

    sales_order.remarks = "\n".join(filter(None, remarks_parts)) if remarks_parts else None

    target_warehouse_field = _get_items_warehouse_field(sales_order)

    for row in item_rows:
        item = sales_order.append("items", {})
        item.item_code = row["item_code"]
        item.item_name = row["item_name"]
        item.description = row["description"]
        item.qty = row["qty"]
        if item.meta.has_field("uom"):
            item.uom = row.get("uom")
        if target_warehouse_field and hasattr(item, target_warehouse_field):
            item.set(target_warehouse_field, warehouse)

    sales_order.set_missing_values()
    sales_order.calculate_taxes_and_totals()
    
    # 设置Shopify订单默认状态为"待发货"（已付款订单）
    sales_order.status = "To Deliver"

    # 直接从原始 Shopify 订单数据中获取 ID
    shopify_order_id = payload.get("id")
    frappe.logger().error(f"[Shopify] 调试 shopify_order_id: 从原始数据获取 = {shopify_order_id}")
    
    # 安全获取 order_doc 中的 shopify_order_id
    order_doc_shopify_id = getattr(order_doc, 'shopify_order_id', None)
    
    # 优先使用原始数据中的 ID，如果为空则使用 order_doc 中的值
    final_shopify_id = shopify_order_id or order_doc_shopify_id
    frappe.logger().error(f"[Shopify] 调试 shopify_order_id: 最终使用的值 = {final_shopify_id}")
    
    _set_optional_field(sales_order, "custom_shopify_order_id", final_shopify_id)
    _set_optional_field(sales_order, "custom_shop_raw_data", json.dumps(payload, ensure_ascii=False))
    
    # 安全获取 order_doc 中的 shop_name
    order_doc_shop_name = getattr(order_doc, 'shop_name', None)
    _set_optional_field(sales_order, "custom_shop_name", order_doc_shop_name)
    contact_email = (
        payload.get("email")
        or (payload.get("customer") or {}).get("email")
        or (payload.get("contactEmail"))
    )
    contact_name = (payload.get("customer") or {}).get("displayName") or (
        (payload.get("shippingAddress") or {}).get("name")
    )

    # 多仓发货标志：根据订单包含的履约地点与本店允许地点的关系判断
    try:
        order_locations = _get_fulfillment_locations(payload)  # set
        # allowed_locations 由上层传入
        if order_locations:
            # 若全部不属于我们，直接拒绝导入
            if order_locations.isdisjoint(allowed_locations):
                frappe.throw(
                    msg=(
                        f"订单 {final_shopify_id} 的履约地点 {list(order_locations)} 均不在允许范围内 "
                        f"{list(allowed_locations)}，已过滤。"
                    ),
                    exc=frappe.ValidationError,
                )
            # 若部分属于我们、部分不属于，则提示客户（勾选标志）
            has_external = bool(order_locations - allowed_locations)
            _set_optional_field(sales_order, "custom_has_multi_warehouse", 1 if has_external else 0)
        else:
            _set_optional_field(sales_order, "custom_has_multi_warehouse", 0)
    except Exception:
        pass

    if contact_email:
        _set_optional_field(sales_order, "custom_contact_email", contact_email)
    if contact_name:
        _set_optional_field(sales_order, "custom_contact_display", contact_name)

    if auto_dispatch:
        if sales_order.docstatus == 0:
            sales_order.submit()
        delivery_payload = sync_order_wdt_yt(sales_order)

    else:
        sales_order.save()

    if hasattr(order_doc, "sync_status"):
        order_doc.sync_status = "Imported"
    if order_doc.meta.has_field("last_import_target"):
        order_doc.last_import_target = sales_order.name

    order_doc.flags.ignore_permissions = True
    order_doc.save(ignore_permissions=True)

    return sales_order.name

def sync_order_wdt_yt(sales_order):
    """同步销售订单到云途并构建旺店通发货单。"""

    # 初始化变量，确保在异常处理中也能访问
    waybill_number = ""
    tracking_number = ""
    label_url = ""
    
    try:
        frappe.db.savepoint("sync_order_wdt")

        customer = frappe.get_doc("Customer", sales_order.customer)

        warehouse = sales_order.set_warehouse or frappe.db.get_single_value("Stock Settings", "default_warehouse")
        if not warehouse:
            raise frappe.ValidationError("销售订单缺少关联仓库，无法创建云途运单")

        company_doc = frappe.get_cached_doc("Company", sales_order.company)
        prefer_private = bool(getattr(company_doc, "custom_is_virtual_trace", False))

        warehouse_address = _get_warehouse_address(warehouse, prefer_private)
        receiver_info = _extract_shopify_shipping_address(sales_order)
        if not receiver_info:
            raise frappe.ValidationError("Shopify 订单缺少收货地址，无法创建云途运单")

        selected_product_code = "THPHR"
        yt_result = create_yunexpress_order(
            sales_order,
            receiver_info,
            warehouse_address,
            selected_product_code,
        )
        if not yt_result.get("success"):
            frappe.db.rollback(save_point="sync_order_wdt")
            raise frappe.ValidationError(f"云途订单创建失败: {yt_result.get('message')}")

        yt_data = yt_result.get("data", {})
        waybill_number = yt_data.get("waybill_number", "")
        tracking_number = yt_data.get("tracking_number", "")
        label_url = yt_data.get("label_url", "")

        if sales_order.meta.has_field("custom_yt_ret_data"):
            sales_order.db_set("custom_yt_ret_data", json.dumps(yt_data, ensure_ascii=False), update_modified=False)
        if sales_order.meta.has_field("custom_delivery_no") and tracking_number:
            sales_order.db_set("custom_delivery_no", tracking_number, update_modified=False)
        if sales_order.meta.has_field("custom_delivery_order_no") and waybill_number:
            sales_order.db_set("custom_delivery_order_no", waybill_number, update_modified=False)
        if sales_order.meta.has_field("custom_label_url") and label_url:
            sales_order.db_set("custom_label_url", label_url, update_modified=False)

        # 对于 Shopify 订单，使用从 Shopify 提取的收货地址信息
        # 不需要从 ERPNext 的 Address 表获取，因为收货地址信息已经在 receiver_info 中
        delivery_data = build_wdt_delivery_data(
            sales_order,
            shipping_address=receiver_info,  # 使用从 Shopify 提取的收货地址
            sender_address=warehouse_address,
            waybill_number=waybill_number,
            tracking_number=tracking_number,
            label_url=label_url,
            logistics_product_code=selected_product_code,
        )

        result = wdt.wdt_wms_deliveryorder_create(delivery_data)
        if isinstance(result, dict) and result.get("code") == "0":
            # 旺店通发货单创建成功，获取仓库订单号
            warehouse_order_no = result.get("data", {}).get("deliveryOrderNo") or result.get("deliveryOrderNo")
            if warehouse_order_no and sales_order.meta.has_field("custom_warehouse_order_no"):
                sales_order.db_set("custom_warehouse_order_no", warehouse_order_no, update_modified=False)
                frappe.logger().info(f"旺店通仓库订单号已设置: {warehouse_order_no}")
            return delivery_data

        # 旺店通发货单创建失败，需要撤销云途订单
        frappe.logger().warning(f"旺店通发货单创建失败，开始撤销云途订单: {waybill_number}")
        cancel_result = cancel_yunexpress_order(waybill_number)
        if not cancel_result.get("success"):
            frappe.logger().error(f"撤销云途订单失败: {cancel_result.get('message')}")
        
        frappe.db.rollback(save_point="sync_order_wdt")
        error_message = result.get("message") if isinstance(result, dict) else str(result)
        raise frappe.ValidationError(f"旺店通发货单创建失败: {error_message}")

    except Exception as e:

        # 如果云途订单已创建，需要撤销云途订单
        if len(waybill_number) > 3:
            frappe.logger().warning(f"发生异常，开始撤销云途订单: {waybill_number}")
            cancel_result = cancel_yunexpress_order(waybill_number)
            if not cancel_result.get("success"):
                frappe.logger().error(f"撤销云途订单失败: {cancel_result.get('message')}")
            else:
                frappe.logger().info(f"云途订单撤销成功: {waybill_number}")
        else:
            frappe.logger().info("云途订单未创建，无需撤销")
        
        frappe.db.rollback(save_point="sync_order_wdt")
        frappe.logger().error(f"构建旺店通发货单数据失败: {str(e)}")
        raise e

def _ensure_customer(customer_payload: Optional[Dict]) -> str:
    return DEFAULT_CUSTOMER


def _ensure_shipping_address(
    *, customer_name: str, address_payload: Optional[Dict], email: Optional[str]
) -> Optional[str]:
    return None


def _get_order_company(shop_name: Optional[str]) -> str:
    if shop_name:
        company = frappe.db.get_value("usershop", shop_name, "company")
        if company:
            return company

    company = get_global_default("company") or frappe.db.get_default("company")
    if company:
        return company

    fallback = frappe.get_all("Company", pluck="name", limit=1)
    if fallback:
        return fallback[0]

    raise frappe.ValidationError("No Company configured for Shopify import")


def _set_optional_field(doc, fieldname: str, value) -> None:
    if value is None:
        return
    if hasattr(doc, "meta") and doc.meta.has_field(fieldname):
        doc.set(fieldname, value)


def _get_warehouse_address(warehouse_name: str, prefer_private: bool):
    warehouse_doc = frappe.get_cached_doc("Warehouse", warehouse_name)

    direct_address = getattr(warehouse_doc, "address", None)
    if direct_address:
        try:
            return frappe.get_cached_doc("Address", direct_address)
        except frappe.DoesNotExistError:
            frappe.logger().error(
                "[Shopify][Orders] 仓库 %s 指定的 address=%s 不存在",
                warehouse_name,
                direct_address,
            )

    linked_names = frappe.get_all(
        "Dynamic Link",
        filters={
            "link_doctype": "Warehouse",
            "link_name": warehouse_name,
            "parenttype": "Address",
        },
        pluck="parent",
    )

    if linked_names:
        priority = ["Private", "Shipping", "Warehouse"] if prefer_private else ["Shipping", "Warehouse", "Private"]

        all_linked = frappe.get_all(
            "Address",
            filters={"name": ["in", linked_names]},
            fields=["name", "address_type", "is_primary_address"],
            order_by="creation desc",
        )
        frappe.logger().debug(
            "[Shopify][Orders] 仓库地址查找: warehouse=%s prefer_private=%s linked_names=%s all_linked=%s",
            warehouse_name,
            prefer_private,
            linked_names,
            all_linked,
        )

        for address_type in priority:
            matched = frappe.get_all(
                "Address",
                filters={
                    "name": ["in", linked_names],
                    "address_type": address_type,
                },
                order_by="is_primary_address desc, creation desc",
                pluck="name",
                limit=1,
            )
            if matched:
                return frappe.get_cached_doc("Address", matched[0])

    fallback = frappe.get_all(
            "Address",
            filters={"name": ["in", linked_names]},
            order_by="is_primary_address desc, creation desc",
        pluck="name",
        limit=1,
    )
    if fallback:
            return frappe.get_cached_doc("Address", fallback[0])

    frappe.logger().warning(
        "[Shopify][Orders] 仓库地址缺失: warehouse=%s prefer_private=%s linked_names=%s",
        warehouse_name,
        prefer_private,
        linked_names,
    )
    raise frappe.ValidationError(
        frappe._("仓库 {0} 没有配置发货地址，请在仓库文档中添加地址").format(warehouse_name)
    )


def _extract_shopify_shipping_address(sales_order):
    payload = getattr(getattr(sales_order, "flags", None), "shopify_payload", None)
    if not payload:
        raw_data = getattr(sales_order, "custom_shop_raw_data", None)
        if raw_data:
            try:
                payload = json.loads(raw_data)
            except json.JSONDecodeError as exc:
                raise frappe.ValidationError(f"无法解析保存的 Shopify 原始数据: {exc}") from exc
    if not payload and getattr(sales_order, "custom_shopify_order_id", None):
        raw_payload = frappe.db.get_value("Shopify Order", {"shopify_order_id": sales_order.custom_shopify_order_id}, "raw_payload")
        if raw_payload:
            try:
                payload = json.loads(raw_payload)
            except json.JSONDecodeError as exc:
                raise frappe.ValidationError(f"无法解析 Shopify 订单 {sales_order.custom_shopify_order_id} 的原始数据: {exc}") from exc

    if not payload:
        raise frappe.ValidationError("缺少 Shopify 原始数据，无法解析收货地址")

    shipping = payload.get("shippingAddress") or payload.get("shipping_address")
    if not shipping:
        raise frappe.ValidationError("Shopify 订单缺少 shippingAddress")

    address_line1 = (shipping.get("address1") or shipping.get("addressLine1") or "").strip()
    city = (shipping.get("city") or "").strip()
    country = shipping.get("country") or shipping.get("countryCode") or shipping.get("countryCodeV2")
    postal_code = shipping.get("zip") or shipping.get("postalCode")

    missing_fields = []
    if not address_line1:
        missing_fields.append("address1")
    if not city:
        missing_fields.append("city")
    if not country:
        missing_fields.append("country")
    if not postal_code:
        missing_fields.append("postal_code")
    if missing_fields:
        raise frappe.ValidationError(
            "Shopify 收货地址缺少必填字段: " + ", ".join(missing_fields)
        )

    province = shipping.get("province") or shipping.get("provinceCode")
    area = shipping.get("county") or shipping.get("countyName")
    country_code = shipping.get("countryCode") or shipping.get("countryCodeV2") or country

    first_name = shipping.get("firstName") or ""
    last_name = shipping.get("lastName") or ""
    display_name = shipping.get("name") or " ".join(part for part in [first_name, last_name] if part)
    if not display_name:
        display_name = sales_order.customer_name or sales_order.customer

    customer_email = (
        payload.get("email")
        or (payload.get("customer") or {}).get("email")
        or (payload.get("contactEmail"))
    )

    return _dict({
        "address_title": display_name,
        "address_line1": address_line1,
        "address_line2": (shipping.get("address2") or shipping.get("addressLine2")),
        "city": city,
        "state": province,
        "country": country,
        "country_code": country_code,
        "pincode": postal_code,
        "phone": shipping.get("phone"),
        "email_id": customer_email,
        "county": area,
        "first_name": first_name,
        "last_name": last_name,
        "address_lines": [
            part
            for part in [address_line1, shipping.get("address2") or shipping.get("addressLine2")]
            if part
        ],
    })


def _get_items_warehouse_field(sales_order) -> Optional[str]:
    child_meta = sales_order.meta.get_field("items")
    if not child_meta or not child_meta.options:
        return None

    items_meta = frappe.get_meta(child_meta.options)
    if not items_meta:
        return None
    for fieldname in ("warehouse", "source_warehouse", "from_warehouse"):
        if items_meta.has_field(fieldname):
            return fieldname

    return "warehouse" if items_meta.has_field("warehouse") else None


@frappe.whitelist()
def update_shopify_fulfillment(
    sales_order_name: str,
    tracking_number: str,
    fulfillment_orders: list,
    tracking_url: str = None,
    carrier: str = None
) -> Dict[str, Any]:
   
    try:
        # 严格参数校验
        if not sales_order_name or not isinstance(sales_order_name, str) or not sales_order_name.strip():
            return {
                "success": False,
                "error": "Sales order name is required and must be a non-empty string"
            }
        
        if not tracking_number or not isinstance(tracking_number, str) or not tracking_number.strip():
            return {
                "success": False,
                "error": "Tracking number is required and must be a non-empty string"
            }
        
        # 从销售订单获取店铺信息
        try:
            sales_order = frappe.get_doc("Sales Order", sales_order_name)
            shop_name = getattr(sales_order, 'custom_shop_name', None)
            
            if not shop_name:
                return {
                    "success": False,
                    "error": f"Sales order {sales_order_name} does not have shop information (custom_shop_name field is empty)"
                }
                
            frappe.logger().info(f"[Shopify Fulfillment] 从订单获取店铺信息 - 订单: {sales_order_name}, 店铺: {shop_name}")
            
        except frappe.DoesNotExistError:
            return {
                "success": False,
                "error": f"Sales order {sales_order_name} not found"
            }
        except Exception as e:
            return {
                "success": False,
                "error": f"Failed to get shop information from sales order: {str(e)}"
            }
        
        if not fulfillment_orders or not isinstance(fulfillment_orders, list) or len(fulfillment_orders) == 0:
            return {
                "success": False,
                "error": "Fulfillment orders is required and must be a non-empty list"
            }
        
        # 校验可选参数
        if tracking_url is not None and (not isinstance(tracking_url, str) or not tracking_url.strip()):
            return {
                "success": False,
                "error": "Tracking URL must be a non-empty string if provided"
            }
        
        if carrier is not None and (not isinstance(carrier, str) or not carrier.strip()):
            return {
                "success": False,
                "error": "Carrier must be a non-empty string if provided"
            }
        
        # 简单校验 fulfillment_orders 格式
        for i, fulfillment_order in enumerate(fulfillment_orders):
            if not isinstance(fulfillment_order, dict):
                return {
                    "success": False,
                    "error": f"Fulfillment order at index {i} must be a dictionary"
                }
            
            # 只校验必要字段存在
            if "fulfillmentOrderId" not in fulfillment_order or "fulfillmentOrderLineItems" not in fulfillment_order:
                return {
                    "success": False,
                    "error": f"Fulfillment order at index {i} must have 'fulfillmentOrderId' and 'fulfillmentOrderLineItems' fields"
                }
            
            # 简单校验 line items 是数组且非空
            line_items = fulfillment_order["fulfillmentOrderLineItems"]
            if not isinstance(line_items, list) or len(line_items) == 0:
                return {
                    "success": False,
                    "error": f"Fulfillment order at index {i} 'fulfillmentOrderLineItems' must be a non-empty list"
                }
        
        
        # 记录操作日志
        frappe.logger().info(
            f"[Shopify Fulfillment] 开始更新履约信息 - 订单: {sales_order_name}, 店铺: {shop_name}, 跟踪号: {tracking_number}, 履约订单数: {len(fulfillment_orders)}"
        )
        
        # 创建Shopify客户端
        try:
            frappe.logger().info(f"[Shopify Fulfillment] 创建客户端 - 请求的shop_name: {shop_name}")
            client = ShopifyClient(shop_name)
            frappe.logger().info(f"[Shopify Fulfillment] 客户端创建成功 - 实际使用的shop: {client.config.name}, shopurl: {client.config.shopurl}")
        except ShopifyClientError as exc:
            frappe.logger().error(f"[Shopify Fulfillment] 客户端创建失败: {str(exc)}")
            return {
                "success": False,
                "error": f"Failed to create Shopify client: {str(exc)}"
            }
        
        # 构建跟踪信息
        tracking_info = {
            "number": tracking_number.strip(),
            "company": carrier.strip() if carrier else "Unknown"
        }
        
        if tracking_url and tracking_url.strip():
            tracking_info["url"] = tracking_url.strip()
        
        # 创建履约记录
        fulfillment_result = _create_fulfillment(
            client, 
            "unknown_order_id",  # 这个函数不需要order_id，因为fulfillment_orders已经包含了订单信息
            fulfillment_orders, 
            tracking_info
        )
        
        if fulfillment_result.get("success"):
            frappe.logger().info(
                f"[Shopify Fulfillment] 履约信息更新成功 - 履约ID: {fulfillment_result.get('fulfillment_id')}"
            )
            return {
                "success": True,
                "message": "Fulfillment updated successfully",
                "fulfillment_id": fulfillment_result.get("fulfillment_id"),
                "tracking_number": tracking_number,
                "carrier": carrier or "Unknown"
            }
        else:
            frappe.logger().error(
                f"[Shopify Fulfillment] 履约信息更新失败 - 错误: {fulfillment_result.get('error')}"
            )
            return {
                "success": False,
                "error": fulfillment_result.get("error", "Failed to create fulfillment")
            }
    
    except Exception as e:
        frappe.logger().error(f"[Shopify Fulfillment] 更新异常: {str(e)}")
        return {
            "success": False,
            "error": f"Failed to update fulfillment: {str(e)}"
        }


def build_fulfillment_orders_from_sales_order(sales_order_name: str) -> list:
    """
    从Sales Order Item中构建fulfillment orders数据
    避免每次都从Shopify API获取
    """
    try:
        sales_order = frappe.get_doc("Sales Order", sales_order_name)
        shopify_order_id = getattr(sales_order, 'custom_shopify_order_id', None)
        
        if not shopify_order_id:
            return []
        
        # 从Sales Order Item中获取Shopify line item ID
        fulfillment_orders = []
        for item in sales_order.items:
            shopify_line_item_id = getattr(item, 'custom_shopify_line_item_id', None)
            if shopify_line_item_id:
                fulfillment_orders.append({
                    "fulfillmentOrderId": shopify_order_id,  # 这里需要fulfillment order ID，不是order ID
                    "fulfillmentOrderLineItems": [{
                        "id": shopify_line_item_id,
                        "quantity": item.qty
                    }]
                })
        
        return fulfillment_orders
        
    except Exception as e:
        frappe.logger().error(f"[Shopify Fulfillment] 从Sales Order构建fulfillment数据失败: {str(e)}")
        return []


def _create_fulfillment(
    client: ShopifyClient, 
    order_id: str, 
    line_items: list, 
    tracking_info: dict
) -> Dict[str, Any]:
    """
    创建Shopify履约记录
    """
    try:
        mutation = """
        mutation FulfillmentCreateV2($fulfillment: FulfillmentV2Input!) {
            fulfillmentCreateV2(fulfillment: $fulfillment) {
                fulfillment {
                    id
                    status
                }
                userErrors {
                    field
                    message
                }
            }
        }
        """
        
        # 直接使用传入的 line_items 数据
        variables = {
            "fulfillment": {
                "lineItemsByFulfillmentOrder": line_items,
                "trackingInfo": tracking_info,
                "notifyCustomer": True
            }
        }
        
        result = client._post_graphql(mutation, variables)
       
        if not result:
            return {
                "success": False,
                "error": "No response from Shopify API"
            }
        
        # client._post_graphql 已返回 data 字段的内容
        fulfillment_create = result.get("fulfillmentCreateV2")
        if not fulfillment_create:
            return {
                "success": False,
                "error": f"No fulfillmentCreateV2 in response: {result}"
            }
        
        user_errors = fulfillment_create.get("userErrors", [])
        
        if user_errors:
            error_messages = [error.get("message", "Unknown error") for error in user_errors]
            return {
                "success": False,
                "error": f"Shopify API errors: {'; '.join(error_messages)}"
            }
        
        fulfillment = fulfillment_create.get("fulfillment", {})
        
        return {
            "success": True,
            "fulfillment_id": fulfillment.get("id"),
            "status": fulfillment.get("status"),
            "tracking_info": fulfillment.get("trackingInfo", {})
        }
        
    except Exception as e:
        frappe.logger().error(f"[Shopify Fulfillment] 创建履约记录失败: {str(e)}")
        return {
            "success": False,
            "error": f"Failed to create fulfillment: {str(e)}"
        }


@frappe.whitelist()
def get_shopify_order_fulfillments(sales_order_name: str) -> Dict[str, Any]:
    """
    根据ERPNext销售订单名称查询Shopify履约信息 - 用于前端人工核验
    
    Args:
        sales_order_name: ERPNext销售订单名称
    
    Returns:
        Dict: 订单履约信息
    """
    try:
        # 参数校验
        if not sales_order_name or not isinstance(sales_order_name, str) or not sales_order_name.strip():
            return {
                "success": False,
                "error": "Sales order name is required and must be a non-empty string"
            }
        
        # 从销售订单获取店铺信息和Shopify订单ID
        try:
            sales_order = frappe.get_doc("Sales Order", sales_order_name)
            shop_name = getattr(sales_order, 'custom_shop_name', None)
            shopify_order_id = getattr(sales_order, 'custom_shopify_order_id', None)
            
            if not shop_name:
                return {
                    "success": False,
                    "error": f"Sales order {sales_order_name} does not have shop information (custom_shop_name field is empty)"
                }
            
            if not shopify_order_id:
                return {
                    "success": False,
                    "error": f"Sales order {sales_order_name} does not have Shopify order ID (custom_shopify_order_id field is empty)"
                }
                
            frappe.logger().info(f"[Shopify Fulfillment] 从订单获取信息 - 订单: {sales_order_name}, 店铺: {shop_name}, Shopify订单: {shopify_order_id}")
            
        except frappe.DoesNotExistError:
            return {
                "success": False,
                "error": f"Sales order {sales_order_name} not found"
            }
        except Exception as e:
            return {
                "success": False,
                "error": f"Failed to get information from sales order: {str(e)}"
            }
        
        # 标准化Shopify订单ID格式
        shopify_order_id = shopify_order_id.strip()
        if not shopify_order_id.startswith("gid://shopify/Order/"):
            if shopify_order_id.isdigit():
                shopify_order_id = f"gid://shopify/Order/{shopify_order_id}"
            else:
                return {
                    "success": False,
                    "error": "Invalid Shopify order ID format. Must be numeric ID or gid://shopify/Order/ format"
                }
        
        # 记录操作日志
        frappe.logger().info(
            f"[Shopify Fulfillment] 查询订单履约信息 - 订单: {sales_order_name}, 店铺: {shop_name}, Shopify订单: {shopify_order_id}"
        )
        
        # 创建Shopify客户端
        try:
            frappe.logger().info(f"[Shopify Fulfillment] 创建客户端 - 请求的shop_name: {shop_name}")
            client = ShopifyClient(shop_name)
            frappe.logger().info(f"[Shopify Fulfillment] 客户端创建成功 - 实际使用的shop: {client.config.name}, shopurl: {client.config.shopurl}")
        except ShopifyClientError as exc:
            frappe.logger().error(f"[Shopify Fulfillment] 客户端创建失败: {str(exc)}")
            return {
                "success": False,
                "error": f"Failed to create Shopify client: {str(exc)}"
            }
        
        # 查询订单履约信息
        fulfillment_data = _get_order_fulfillments(client, shopify_order_id)
        
        if not fulfillment_data:
            return {
                "success": False,
                "error": "Failed to retrieve fulfillment data"
            }
        
        return {
            "success": True,
            "data": fulfillment_data,
            "message": "Fulfillment data retrieved successfully"
        }
    
    except Exception as e:
        frappe.logger().error(f"[Shopify Fulfillment] 查询履约信息异常: {str(e)}")
        return {
            "success": False,
            "error": f"Failed to get fulfillment data: {str(e)}"
        }


def _get_order_fulfillments(client: ShopifyClient, order_id: str) -> Dict[str, Any]:
    """
    查询订单的履约信息
    """
    try:
        query = """
        query GetOrderFulfillments($orderId: ID!) {
            order(id: $orderId) {
                id
                name
                displayFinancialStatus
                displayFulfillmentStatus
                fulfillments(first: 10) {
                    id
                    status
                    createdAt
                    updatedAt
                    trackingInfo {
                        number
                        url
                        company
                    }
                    fulfillmentLineItems(first: 50) {
                        edges {
                            node {
                                id
                                quantity
                                lineItem {
                                    id
                                    title
                                    variant {
                                        id
                                        sku
                                    }
                                }
                            }
                        }
                    }
                }
                fulfillmentOrders(first: 10) {
                    edges {
                        node {
                            id
                            status
                            assignedLocation {
                                name
                            }
                            destination {
                                id
                                address1
                                city
                                zip
                            }
                            lineItems(first: 50) {
                                edges {
                                    node {
                                        id
                                        remainingQuantity
                                        totalQuantity
                                        lineItem {
                                            id
                                            title
                                            variant {
                                                id
                                                sku
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        """
        
        result = client._post_graphql(query, {"orderId": order_id})
                
        if not result or "order" not in result:
            frappe.logger().error(f"[Shopify Fulfillment] 订单查询失败: {order_id}")
            return None
        
        order = result["order"]
        
        # 处理履约记录
        fulfillments = []
        for fulfillment in order.get("fulfillments", []):
            fulfillment_line_items = []
            
            for line_edge in fulfillment.get("fulfillmentLineItems", {}).get("edges", []):
                line_item = line_edge["node"]
                fulfillment_line_items.append({
                    "id": line_item["id"],
                    "quantity": line_item["quantity"],
                    "line_item": {
                        "id": line_item["lineItem"]["id"],
                        "title": line_item["lineItem"]["title"],
                        "variant": {
                            "id": line_item["lineItem"]["variant"]["id"],
                            "sku": line_item["lineItem"]["variant"]["sku"]
                        }
                    }
                })
            
            fulfillments.append({
                "id": fulfillment["id"],
                "status": fulfillment["status"],
                "created_at": fulfillment["createdAt"],
                "updated_at": fulfillment["updatedAt"],
                # Normalize trackingInfo: Shopify returns a list; UI expects a single object
                "tracking_info": (
                    (fulfillment.get("trackingInfo") or [{}])[0]
                    if isinstance(fulfillment.get("trackingInfo"), list)
                    else (fulfillment.get("trackingInfo") or {})
                ),
                "line_items": fulfillment_line_items
            })
        
        # 处理履约订单
        fulfillment_orders = []
        for edge in order.get("fulfillmentOrders", {}).get("edges", []):
            fulfillment_order = edge["node"]
            fulfillment_order_line_items = []
            
            for line_edge in fulfillment_order.get("lineItems", {}).get("edges", []):
                line_node = line_edge["node"]
                fulfillment_order_line_items.append({
                    "id": line_node["id"],
                    "remaining_quantity": line_node["remainingQuantity"],
                    "total_quantity": line_node["totalQuantity"],
                    "line_item": {
                        "id": line_node["lineItem"]["id"],
                        "title": line_node["lineItem"]["title"],
                        "variant": {
                            "id": line_node["lineItem"]["variant"]["id"],
                            "sku": line_node["lineItem"]["variant"]["sku"]
                        }
                    }
                })
            
            fulfillment_orders.append({
                "id": fulfillment_order["id"],
                "status": fulfillment_order["status"],
                "assigned_location": fulfillment_order.get("assignedLocation"),
                "destination": fulfillment_order.get("destination"),
                "line_items": fulfillment_order_line_items
            })
        
        return {
            "order": {
                "id": order["id"],
                "name": order["name"],
                "financial_status": order["displayFinancialStatus"],
                "fulfillment_status": order["displayFulfillmentStatus"]
            },
            "fulfillments": fulfillments,
            "fulfillment_orders": fulfillment_orders
        }
        
    except Exception as e:
        frappe.logger().error(f"[Shopify Fulfillment] 查询履约信息失败: {str(e)}")
        frappe.logger().error(f"[Shopify Fulfillment] 异常详情: {type(e).__name__}: {str(e)}")
        import traceback
        frappe.logger().error(f"[Shopify Fulfillment] 堆栈跟踪: {traceback.format_exc()}")
        return None


@frappe.whitelist()
def update_shopify_fulfillment_tracking(
    sales_order_name: str,
    tracking_number: str,
    tracking_url: str = None,
    carrier: str = None
) -> Dict[str, Any]:
    """
    更新Shopify履约的物流跟踪信息
    
    Args:
        sales_order_name: ERPNext销售订单名称
        tracking_number: 新的物流跟踪号
        tracking_url: 物流跟踪链接
        carrier: 物流公司名称
    
    Returns:
        Dict: 更新结果
    """
    try:
        # 参数校验
        if not sales_order_name or not isinstance(sales_order_name, str) or not sales_order_name.strip():
            return {
                "success": False,
                "error": "Sales order name is required and must be a non-empty string"
            }
        
        # 从销售订单获取店铺信息和Shopify订单ID
        try:
            sales_order = frappe.get_doc("Sales Order", sales_order_name)
            shop_name = getattr(sales_order, 'custom_shop_name', None)
            shopify_order_id = getattr(sales_order, 'custom_shopify_order_id', None)
            
            if not shop_name:
                return {
                    "success": False,
                    "error": f"Sales order {sales_order_name} does not have shop information (custom_shop_name field is empty)"
                }
            
            if not shopify_order_id:
                return {
                    "success": False,
                    "error": f"Sales order {sales_order_name} does not have Shopify order ID (custom_shopify_order_id field is empty)"
                }
                
            frappe.logger().info(f"[Shopify Fulfillment] 从订单获取信息 - 订单: {sales_order_name}, 店铺: {shop_name}, Shopify订单: {shopify_order_id}")
            
        except frappe.DoesNotExistError:
            return {
                "success": False,
                "error": f"Sales order {sales_order_name} not found"
            }
        except Exception as e:
            return {
                "success": False,
                "error": f"Failed to get information from sales order: {str(e)}"
            }
        
        if not tracking_number or not isinstance(tracking_number, str) or not tracking_number.strip():
            return {
                "success": False,
                "error": "Tracking number is required and must be a non-empty string"
            }
        
        # 标准化Shopify订单ID格式
        shopify_order_id = shopify_order_id.strip()
        if not shopify_order_id.startswith("gid://shopify/Order/"):
            if shopify_order_id.isdigit():
                shopify_order_id = f"gid://shopify/Order/{shopify_order_id}"
            else:
                return {
                    "success": False,
                    "error": "Invalid Shopify order ID format. Must be numeric ID or gid://shopify/Order/ format"
                }
        
        # 记录操作日志
        frappe.logger().info(
            f"[Shopify Fulfillment] 更新物流跟踪信息 - 店铺: {shop_name}, 订单: {shopify_order_id}, 跟踪号: {tracking_number}"
        )
        
        # 创建Shopify客户端
        try:
            client = ShopifyClient(shop_name)
        except ShopifyClientError as exc:
            frappe.logger().error(f"[Shopify Fulfillment] 客户端创建失败: {str(exc)}")
            return {
                "success": False,
                "error": f"Failed to create Shopify client: {str(exc)}"
            }
        
        # 更新履约跟踪信息
        update_result = _update_fulfillment_tracking(client, shopify_order_id, tracking_number, tracking_url, carrier)
        
        if update_result.get("success"):
            frappe.logger().info(
                f"[Shopify Fulfillment] 物流跟踪信息更新成功 - 订单: {shopify_order_id}, 跟踪号: {tracking_number}"
            )
            return {
                "success": True,
                "message": "Fulfillment tracking updated successfully",
                "tracking_number": tracking_number,
                "carrier": carrier or "Unknown"
            }
        else:
            frappe.logger().error(
                f"[Shopify Fulfillment] 物流跟踪信息更新失败 - 订单: {shopify_order_id}, 错误: {update_result.get('error')}"
            )
            return {
                "success": False,
                "error": update_result.get("error", "Failed to update fulfillment tracking")
            }
    
    except Exception as e:
        frappe.logger().error(f"[Shopify Fulfillment] 更新物流跟踪信息异常: {str(e)}")
        return {
            "success": False,
            "error": f"Failed to update fulfillment tracking: {str(e)}"
        }


def _update_fulfillment_tracking(
    client: ShopifyClient, 
    order_id: str, 
    tracking_number: str,
    tracking_url: str = None,
    carrier: str = None
) -> Dict[str, Any]:
    """
    更新履约的物流跟踪信息
    """
    try:
        # 首先获取订单的履约信息
        fulfillment_data = _get_order_fulfillments(client, order_id)
        
        
        if not fulfillment_data:
            return {
                "success": False,
                "error": "Failed to get fulfillment data from Shopify"
            }
        
        fulfillments = fulfillment_data.get("fulfillments", [])
        if fulfillments:
            fulfillments = [
                f
                for f in fulfillments
                if (str(f.get("status") or "").upper() not in {"CANCELLED", "FAILURE"})
            ]
        
        if not fulfillments:
            # 如果没有履约记录，但有履约订单，则创建新的履约
            fulfillment_orders = fulfillment_data.get("fulfillment_orders", [])
            if not fulfillment_orders:
                return {
                    "success": False,
                    "error": "No fulfillments or fulfillment orders found for this order"
                }
            
            # 按地点过滤，仅对目标地点发货（从 usershop.allowed_locations 读取，空则不过滤）
            target_location_names = _get_target_location_names(getattr(client, "shop_name", None))
            if target_location_names:
                filtered_fulfillment_orders = [
                    fo for fo in fulfillment_orders
                    if (fo.get("assigned_location") or {}).get("name") in target_location_names
                ]
            else:
                filtered_fulfillment_orders = fulfillment_orders

            if not filtered_fulfillment_orders:
                return {
                    "success": False,
                    "error": "No fulfillment orders at target location"
                }
            
            # 构建履约订单数据
            fulfillment_orders_data = []
            for fo in filtered_fulfillment_orders:
                fulfillment_order_line_items = []
                for line_item in fo.get("line_items", []):
                    qty = line_item.get("remaining_quantity", line_item.get("total_quantity", 1))
                    if qty and qty > 0:
                        fulfillment_order_line_items.append({
                            "id": line_item["id"],
                            "quantity": qty
                        })
                
                if fulfillment_order_line_items:
                    fulfillment_orders_data.append({
                        "fulfillmentOrderId": fo["id"],
                        "fulfillmentOrderLineItems": fulfillment_order_line_items
                    })
            
            if not fulfillment_orders_data:
                return {
                    "success": False,
                    "error": "No line items to fulfill at target location"
                }
            
            
            # 创建新履约
            tracking_info = {
                "number": tracking_number.strip(),
                "company": carrier.strip() if carrier else "Unknown"
            }
            if tracking_url and tracking_url.strip():
                tracking_info["url"] = tracking_url.strip()
            
            create_result = _create_fulfillment(client, order_id, fulfillment_orders_data, tracking_info)
            
            if create_result.get("success"):
                return {
                    "success": True,
                    "message": "New fulfillment created successfully",
                    "tracking_number": tracking_number,
                    "carrier": carrier or "Unknown"
                }
            else:
                frappe.logger().error(f"[Shopify Fulfillment] 新履约创建失败 - 订单: {order_id}, 错误: {create_result.get('error')}")
                return {
                    "success": False,
                    "error": f"Failed to create fulfillment: {create_result.get('error')}"
                }
        
        # 更新每个履约的跟踪信息
        updated_fulfillments = []
        for fulfillment in fulfillments:
            fulfillment_id = fulfillment["id"]
            
            # 构建跟踪信息
            tracking_info = {
                "number": tracking_number.strip(),
                "company": carrier.strip() if carrier else "Unknown"
            }
            
            if tracking_url and tracking_url.strip():
                tracking_info["url"] = tracking_url.strip()
            
            # 使用 fulfillmentTrackingUpdate 突变
            mutation = """
            mutation FulfillmentTrackingUpdate($id: ID!, $trackingInfo: FulfillmentTrackingInput!) {
                fulfillmentTrackingUpdate(id: $id, trackingInfo: $trackingInfo) {
                    fulfillment {
                        id
                        trackingInfo {
                            number
                            url
                            company
                        }
                    }
                    userErrors {
                        field
                        message
                    }
                }
            }
            """
            
            variables = {
                "id": fulfillment_id,
                "trackingInfo": tracking_info
            }
            
            result = client._post_graphql(mutation, variables)
            
            if not result or "data" not in result or "fulfillmentTrackingUpdate" not in result["data"]:
                return {
                    "success": False,
                    "error": "Invalid response from Shopify API"
                }
            
            fulfillment_update = result["data"]["fulfillmentTrackingUpdate"]
            user_errors = fulfillment_update.get("userErrors", [])
            
            if user_errors:
                error_messages = [error.get("message", "Unknown error") for error in user_errors]
                return {
                    "success": False,
                    "error": f"Shopify API errors: {'; '.join(error_messages)}"
                }
            
            updated_fulfillments.append(fulfillment_update.get("fulfillment", {}))
        
        return {
            "success": True,
            "updated_fulfillments": updated_fulfillments
        }
        
    except Exception as e:
        frappe.logger().error(f"[Shopify Fulfillment] 更新履约跟踪信息失败: {str(e)}")
        return {
            "success": False,
            "error": f"Failed to update fulfillment tracking: {str(e)}"
        }


def _get_target_location_names(shop_name: str) -> set:
    """Return configured Shopify location names from usershop.allowed_warehouses.
    Mandatory: if missing/invalid/empty, raise ValidationError to stop the flow.
    """
    try:
        shop_doc = frappe.get_doc("usershop", shop_name)
    except Exception:
        frappe.throw(msg=f"未找到店铺配置(usershop): {shop_name}", exc=frappe.ValidationError)

    raw = getattr(shop_doc, "allowed_warehouses", None)
    if not raw:
        frappe.throw(msg=f"店铺 {shop_name} 未配置 allowed_warehouses（JSON 数组），无法执行履约发货。",
                     exc=frappe.ValidationError)

    # If the field is already a list (DocField of type JSON), accept directly
    if isinstance(raw, (list, tuple)):
        result = {str(x).strip() for x in raw if str(x).strip()}
        if not result:
            frappe.throw(msg=f"店铺 {shop_name} 的 allowed_warehouses 为空，无法执行履约发货。",
                         exc=frappe.ValidationError)
        return result

    # If stored as string, only accept valid JSON array
    try:
        parsed = json.loads(raw) if isinstance(raw, str) else []
        if isinstance(parsed, list):
            result = {str(x).strip() for x in parsed if str(x).strip()}
            if not result:
                frappe.throw(msg=f"店铺 {shop_name} 的 allowed_warehouses 为空，无法执行履约发货。",
                             exc=frappe.ValidationError)
            return result
        frappe.throw(msg=f"店铺 {shop_name} 的 allowed_warehouses 不是有效的 JSON 数组。",
                     exc=frappe.ValidationError)
    except Exception:
        frappe.throw(msg=f"店铺 {shop_name} 的 allowed_warehouses 解析失败（需为 JSON 数组）。",
                     exc=frappe.ValidationError)


def _get_fulfillment_locations(order: Dict) -> set:
    """Collect all assigned location names from fulfillmentOrders of an order."""
    locations: set = set()
    edges = (order.get("fulfillmentOrders") or {}).get("edges", [])
    for edge in edges:
        node = edge.get("node", {})
        assigned = node.get("assignedLocation") or {}
        name = assigned.get("name")
        if name:
            locations.add(str(name))
    return locations


def _get_line_item_location_map(order: Dict) -> Dict[str, str]:
    """Return a mapping from Shopify LineItem GID to assigned location name.
    Reads fulfillmentOrders->lineItems and builds a dict {lineItem.id: assignedLocation.name}.
    """
    mapping: Dict[str, str] = {}
    for edge in (order.get("fulfillmentOrders") or {}).get("edges", []):
        node = edge.get("node", {})
        loc = ((node.get("assignedLocation") or {}).get("name"))
        if not loc:
            continue
        for li_edge in (node.get("lineItems") or {}).get("edges", []):
            li_node = li_edge.get("node", {})
            line_item_id = ((li_node.get("lineItem") or {}).get("id"))
            if line_item_id:
                mapping[str(line_item_id)] = str(loc)
    return mapping





